////////////////////////////////////////////////////////////////////////////////
//
//  ADOBE SYSTEMS INCORPORATED
//  Copyright 2003-2008 Adobe Systems Incorporated
//  All Rights Reserved.
//
//  NOTICE: Adobe permits you to use, modify, and distribute this file
//  in accordance with the terms of the license agreement accompanying it.
//
////////////////////////////////////////////////////////////////////////////////
package mx.geom
{
    import __AS3__.vec.Vector;
    
    import flash.events.Event;
    import flash.geom.Matrix;
    import flash.geom.Matrix3D;
    import flash.geom.Point;
    import flash.geom.Vector3D;
    
    import mx.core.AdvancedLayoutFeatures;
    import mx.utils.MatrixUtil;

    /**
     *  A CompoundTransform represents a 2D or 3D matrix transform.  A compound transform represents a matrix that can be queried or set either as a 2D matrix,
     *  a 3D matrix, or as individual convenience transform properties such as x, y, scaleX, rotationZ, etc. 
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public class CompoundTransform
    {
    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------
    /**
     *  Constructor.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
        public function CompoundTransform()
        {
        }
        
            
    
    /**
     * @private
     * storage for transform properties. These values are concatenated together with the layout properties to
     * form the actual computed matrix used to render the object.
     */
    private var _rotationX:Number = 0;
    private var _rotationY:Number = 0;
    private var _rotationZ:Number = 0;
    private var _scaleX:Number = 1;
    private var _scaleY:Number = 1;
    private var _scaleZ:Number = 1;     
    private var _x:Number = 0;
    private var _y:Number = 0;
    private var _z:Number = 0;
    
    private var _transformX:Number = 0;
    private var _transformY:Number = 0;
    private var _transformZ:Number = 0;
        
    
    /**
     * @private
     * slots for the 2D and 3D matrix transforms.  Note that 
     * these are only allocated and computed on demand -- many component instances will never use a 3D
     * matrix, for example. 
     */
    private var _matrix:Matrix;
    private var _matrix3D:Matrix3D;
    
    
    /**
     * @private
     * bit field flags for indicating which transforms are valid -- the layout properties, the matrices,
     * and the 3D matrices.  Since developers can set any of the three programmatically, the last one set
     * will always be valid, and the others will be invalid until validated on demand.
     */
    private static const MATRIX_VALID:uint      = 0x20;
    private static const MATRIX3D_VALID:uint        = 0x40;
    private static const PROPERTIES_VALID:uint  = 0x80;
    
    
    /**
     * @private
     * flags for tracking whether the  transform is 3D. A transform is 3D if any of the 3D properties -- rotationX/Y, scaleZ, or z -- are set.
     */
    private static const IS_3D:uint                 = 0x200;
    private static const M3D_FLAGS_VALID:uint           = 0x400;
                
    /**
     * @private
     * constants to indicate which form of a transform -- the properties, matrix, or matrix3D -- is
     *  'the source of truth.'   
     */
    public static const SOURCE_PROPERTIES:uint          = 1;
    /**
     * @private
     * constants to indicate which form of a transform -- the properties, matrix, or matrix3D -- is
     *  'the source of truth.'   
     */
    public static const SOURCE_MATRIX:uint          = 2;
    /**
     * @private
     * constants to indicate which form of a transform -- the properties, matrix, or matrix3D -- is
     *  'the source of truth.'   
     */
    public static const SOURCE_MATRIX3D:uint            = 3;
    
    /**
     * @private
     * indicates the 'source of truth' for the transform.  
     */
    public var sourceOfTruth:uint = SOURCE_PROPERTIES;
    
    /**
     * @private
     * general storage for all of ur flags.  
     */
    private var _flags:uint =  PROPERTIES_VALID;
    
    /**
     * @private
     * flags that get passed to the invalidate method indicating why the invalidation is happening.
     */
    private static const INVALIDATE_FROM_NONE:uint =            0;                      
    private static const INVALIDATE_FROM_PROPERTY:uint =        4;                      
    private static const INVALIDATE_FROM_MATRIX:uint =          5;                      
    private static const INVALIDATE_FROM_MATRIX3D:uint =        6;                      

    /**
     * @private
     * static data used by utility methods below
     */
    private static var decomposition:Vector.<Number> = new Vector.<Number>();
    decomposition.push(0);
    decomposition.push(0);
    decomposition.push(0);
    decomposition.push(0);
    decomposition.push(0);

    private static const RADIANS_PER_DEGREES:Number = Math.PI / 180;

    //----------------------------------------------------------------------------
    
    /**
     *  The x value of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set x(value:Number):void
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _x)
            return;
        translateBy(value-_x,0,0);
        invalidate(INVALIDATE_FROM_PROPERTY, false /*affects3D*/);
    }
    /**
     * @private
     */
    public function get x():Number
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        return _x;
    }
    
    /**
     *  The y value of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set y(value:Number):void
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _y)
            return;
        translateBy(0,value-_y,0);
        invalidate(INVALIDATE_FROM_PROPERTY, false /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get y():Number
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        return _y;
    }
    
    /**
     *  The z value of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set z(value:Number):void
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _z)
            return;
        translateBy(0,0,value-_z);
        invalidate(INVALIDATE_FROM_PROPERTY, true /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get z():Number
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        return _z;
    }
    
    //------------------------------------------------------------------------------

    
    /**
     *  The rotationX, in degrees, of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set rotationX(value:Number):void
    {
        // clamp the rotation value between -180 and 180.  This is what 
        // the Flash player does, so let's mimic it here too.
        value = MatrixUtil.clampRotation(value);
            
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _rotationX)
            return;
        _rotationX = value;
        invalidate(INVALIDATE_FROM_PROPERTY, true /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get rotationX():Number
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        return _rotationX;
    }
    
    /**
     *  The rotationY, in degrees, of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set rotationY(value:Number):void
    {
        // clamp the rotation value between -180 and 180.  This is what 
        // the Flash player does, so let's mimic it here too.
        value = MatrixUtil.clampRotation(value);
        
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _rotationY)
            return;
        _rotationY = value;
        invalidate(INVALIDATE_FROM_PROPERTY, true /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get rotationY():Number
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        return _rotationY;
    }
    
    /**
     *  The rotationZ, in degrees, of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set rotationZ(value:Number):void
    {
        // clamp the rotation value between -180 and 180.  This is what 
        // the Flash player does, so let's mimic it here too.
        value = MatrixUtil.clampRotation(value);
        
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _rotationZ)
            return;
        _rotationZ = value;
        invalidate(INVALIDATE_FROM_PROPERTY, false /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get rotationZ():Number
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        return _rotationZ;
    }
    
    //------------------------------------------------------------------------------
    
    
    /**
     *  The scaleX of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set scaleX(value:Number):void
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _scaleX)
            return;
        _scaleX = value;
        invalidate(INVALIDATE_FROM_PROPERTY, false /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get scaleX():Number
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        return _scaleX;
    }
    
    /**
     *  The scaleY of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set scaleY(value:Number):void
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _scaleY)
            return;
        _scaleY = value;
        invalidate(INVALIDATE_FROM_PROPERTY, false /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get scaleY():Number
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        return _scaleY;
    }
    
    
    /**
     *  The scaleZ of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set scaleZ(value:Number):void
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _scaleZ)
            return;
        _scaleZ = value;
        invalidate(INVALIDATE_FROM_PROPERTY, true /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get scaleZ():Number
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        return _scaleZ;
    }
    

    /**
     * @private
     * returns true if the transform has 3D values.
     */
    public function get is3D():Boolean
    {
        if ((_flags & M3D_FLAGS_VALID) == 0)
            update3DFlags();
        return ((_flags & IS_3D) != 0);
    }
    
    //------------------------------------------------------------------------------
    /**
     *  The x value of the transform center. The transform center is kept fixed as rotation and scale are applied. 
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set transformX(value:Number):void
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _transformX)
            return;
        _transformX = value;
        invalidate(INVALIDATE_FROM_PROPERTY, true /*affects3D*/);
    }

    /**
     * @private
     */
    public function get transformX():Number
    {
        return _transformX;
    }

    //------------------------------------------------------------------------------
    /**
     *  The y value of the tansform center. The transform center is kept fixed as rotation and scale are applied. 
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set transformY(value:Number):void
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _transformY)
            return;
        _transformY = value;
        invalidate(INVALIDATE_FROM_PROPERTY, true /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get transformY():Number
    {
        return _transformY;
    }
    //------------------------------------------------------------------------------
    /**
     *  The z value of the tansform center. The transform center is kept fixed as rotation and scale are applied. 
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function set transformZ(value:Number):void
    {
        if ((_flags & PROPERTIES_VALID) == false) validatePropertiesFromMatrix();
        if (value == _transformZ)
            return;
        _transformZ = value;
        invalidate(INVALIDATE_FROM_PROPERTY, true /*affects3D*/);
    }
    
    /**
     * @private
     */
    public function get transformZ():Number
    {
        return _transformZ;
    }

    //------------------------------------------------------------------------------
    
    /**
     * @private
     * invalidates our various cached values.  Any change to the CompoundTransform object that affects
     * the various transforms should call this function. 
     * @param reason - the code indicating what changes to cause the invalidation.
     * @param affects3D - a flag indicating whether the change affects the 2D/3D nature of the various transforms.
     * @param dispatchChangeEvent - if true, the CompoundTransform will dispatch a change indicating that its underlying transforms
     * have been modified. 
     */
    private function invalidate(reason:uint, affects3D:Boolean):void
    {
        //race("invalidating: " + reason);
        switch(reason)
        {
            case INVALIDATE_FROM_PROPERTY:
                sourceOfTruth = SOURCE_PROPERTIES;
                _flags |= PROPERTIES_VALID;
                _flags &= ~MATRIX_VALID;
                _flags &= ~MATRIX3D_VALID;
                break;
            case INVALIDATE_FROM_MATRIX:
                sourceOfTruth = SOURCE_MATRIX;
                _flags |= MATRIX_VALID;
                _flags &= ~PROPERTIES_VALID;
                _flags &= ~MATRIX3D_VALID;
                break;
            case INVALIDATE_FROM_MATRIX3D:
                sourceOfTruth = SOURCE_MATRIX3D;
                _flags |= MATRIX3D_VALID;
                _flags &= ~PROPERTIES_VALID;
                _flags &= ~MATRIX_VALID;
                break;
        }                       
        if (affects3D)
            _flags &= ~M3D_FLAGS_VALID;
            
    }
    
    private static const EPSILON:Number = .001;
    /**
     * @private
     * updates the flags that indicate whether the layout, offset, and/or computed transforms are 3D in nature.  
     * Since the user can set either the individual transform properties or the matrices directly, we compute these 
     * flags based on what the current 'source of truth' is for each of these values.
     */
    private function update3DFlags():void
    {           
        if ((_flags & M3D_FLAGS_VALID) == 0)
        {
            var matrixIs3D:Boolean = false;

            switch(sourceOfTruth)
            {
                case SOURCE_PROPERTIES:
                    matrixIs3D = ( // note that rotationZ is the same as rotation, and not a 3D affecting                           
                        (Math.abs(_scaleZ-1) > EPSILON) ||  // property.
                        ((Math.abs(_rotationX)+EPSILON)%360) > 2*EPSILON ||
                        ((Math.abs(_rotationY)+EPSILON)%360) > 2*EPSILON ||
                        Math.abs(_z) > EPSILON
                        );
                    break;
                case SOURCE_MATRIX:
                    matrixIs3D = false;
                    break;
                case SOURCE_MATRIX3D:
                    var rawData:Vector.<Number> = _matrix3D.rawData;                    
                    matrixIs3D = (rawData[2] != 0 ||        // rotation y 
                                    rawData[6] != 0 ||      // rotation x
                                    rawData[8] !=0 ||       // rotation y
                                    rawData[10] != 1 ||     // scalez / rotation x / rotation y
                                    rawData[14] != 0);      // translation z
                    break;                              
            }

            if (matrixIs3D)
                _flags |= IS_3D;
            else
                _flags &= ~IS_3D;
                
            _flags |= M3D_FLAGS_VALID;
        }
    }
    
    
    /** 
     *  Applies the delta to the transform's translation component. Unlike setting the x, y, or z properties directly,
     *  this method can be safely called without changing the transform's concept of 'the source of truth'.
     * 
     *  @param x The x value of the transform.
     *  
     *  @param y The y value of the transform.
     *  
     *  @param z The z value of the transform.
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function translateBy(x:Number,y:Number,z:Number = 0):void
    {
        if (_flags & MATRIX_VALID)
        {
            _matrix.tx += x;
            _matrix.ty += y;
        }
        if (_flags & PROPERTIES_VALID)
        {
            _x += x;
            _y += y;
            _z += z;
        }
        if (_flags & MATRIX3D_VALID)
        {
            var data:Vector.<Number> = _matrix3D.rawData;
            data[12] += x;
            data[13] += y;
            data[14] += z;
            _matrix3D.rawData = data;           
        }   
        invalidate(INVALIDATE_FROM_NONE, z != 0 /*affects3D*/);
    }
    

    /**
     *  The 2D matrix either set directly by the user, or composed by combining the transform center, scale, rotation
     *  and translation, in that order.  
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function get matrix():Matrix
    {
        
        if (_flags & MATRIX_VALID)
            return _matrix;
            
        if ((_flags & PROPERTIES_VALID) == false)
            validatePropertiesFromMatrix();
            
        var m:Matrix = _matrix;
        if (m == null)
            m = _matrix = new Matrix();
        else
            m.identity();
    
        AdvancedLayoutFeatures.build2DMatrix(m,_transformX,_transformY,
                      _scaleX,_scaleY,
                      _rotationZ,
                      _x,_y);   
        _flags |= MATRIX_VALID;
        return m;       
    }
        
    /**
     * @private
     */ 
    public function set matrix(v:Matrix):void
    {
        if (_matrix== null)
        {
            _matrix = v.clone();
        }
        else
        {
            _matrix.identity(); 
            _matrix.concat(v);          
        }

        // affects3D is true since setting matrix changes the transform to 2D
        invalidate(INVALIDATE_FROM_MATRIX, true /*affects3D*/);
    }
    
    /**
     * @private
     * decomposes the offset transform matrices down into the convenience offset properties. Note that this is not
     * a bi-directional transformation -- it is possible to create a matrix that can't be fully represented in the
     * convenience properties. This function will pull from the matrix or matrix3D values, depending on which was most
     * recently set
     */
    private function validatePropertiesFromMatrix():void
    {       
        if (sourceOfTruth == SOURCE_MATRIX3D)
        {
            var result:Vector.<Vector3D> = _matrix3D.decompose();
            _rotationX = result[1].x / RADIANS_PER_DEGREES;
            _rotationY = result[1].y / RADIANS_PER_DEGREES;
            _rotationZ = result[1].z / RADIANS_PER_DEGREES;
            _scaleX = result[2].x;
            _scaleY = result[2].y;
            _scaleZ = result[2].z;

            if (_transformX != 0 || _transformY != 0 || _transformZ != 0)
            {
                var postTransformTCenter:Vector3D = _matrix3D.transformVector(new Vector3D(_transformX,_transformY,_transformZ));
                _x = postTransformTCenter.x - _transformX;
                _y = postTransformTCenter.y - _transformY;
                _z = postTransformTCenter.z - _transformZ;
            }
            else
            {
                _x = result[0].x;
                _y = result[0].y;
                _z = result[0].z;
            }
        }                        
        else if (sourceOfTruth == SOURCE_MATRIX)
        {
            MatrixUtil.decomposeMatrix(decomposition,_matrix,_transformX,_transformY);
            _x = decomposition[0];
            _y = decomposition[1];
            _z = 0;
            _rotationX = 0;
            _rotationY = 0;
            _rotationZ = decomposition[2];
            _scaleX = decomposition[3];
            _scaleY = decomposition[4];
            _scaleZ = 1;
        }
        _flags |= PROPERTIES_VALID;
        
    }
    
    
    
    /**
     *  The 3D matrix either set directly by the user, or composed by combining the transform center, scale, rotation
     *  and translation, in that order. 
     *  
     *  @langversion 3.0
     *  @playerversion Flash 9
     *  @playerversion AIR 1.1
     *  @productversion Flex 3
     */
    public function get matrix3D():Matrix3D
    {
        if (_flags & MATRIX3D_VALID)
            return _matrix3D;
        
        if ((_flags & PROPERTIES_VALID) == false)
            validatePropertiesFromMatrix();
                            
        var m:Matrix3D = _matrix3D;
        if (m == null)
            m =  _matrix3D = new Matrix3D();
        else
            m.identity();
        
            AdvancedLayoutFeatures.build3DMatrix(m,transformX,transformY,transformZ,
                      _scaleX,_scaleY,_scaleZ,
                      _rotationX,_rotationY,_rotationZ,                       
                      _x,_y,_z);
        _flags |= MATRIX3D_VALID;
        return m;
        
    }
    
    /**
     * @private
     */
    public function set matrix3D(v:Matrix3D):void
    {
        if (_matrix3D == null)
        {
            _matrix3D = v.clone();
        }
        else
        {
            _matrix3D.identity();
            if (v)
                _matrix3D.append(v);            
        }
        invalidate(INVALIDATE_FROM_MATRIX3D, true /*affects3D*/);
    }

}
}